using System;
using System.Collections.Generic;
using System.Text;

using System.Net.Sockets;
using System.Xml;

using System.Threading;


namespace OWED.Network
{
    public class ConnectionException: Exception
    {
        public ConnectionException(String msg) : base(msg) { }
    }
    public delegate void OWEDReply(XmlDocument reply);

    public delegate void ConnectionExceptionHandler(ConnectionException exc);

    /// <summary>
    /// A class for handling Client connections to a compatible server.
    /// This class deals with asynchronously passing Xml document messages
    /// over sockets.
    /// </summary>
    public class Connection:IDisposable
    {
        private Socket my_socket;
        private List<ArraySegment<byte>> my_recv;
        private Dictionary<int, OWEDReply> my_handles;
        int my_port;


        private bool am_valid;
        private bool synchr_really_done;

        int my_number;
        private Thread my_listen;

        ConnectionExceptionHandler my_exc_handle;

        public Connection(String hostName, int port,
            ConnectionExceptionHandler exception_handler)
            : this(hostName, port)
        {
            my_exc_handle = exception_handler;
        }

        /// <summary>
        /// Only constructor.
        /// </summary>
        /// <param name="hostName">The host you wish to connect to</param>
        /// <param name="port">The port number</param>
        public Connection(String hostName, int port)
        {
            my_number = 0;
            my_port = port;
            my_recv = new List<ArraySegment<byte>>();
            my_handles = new Dictionary<int, OWEDReply>();
            
            my_socket = new Socket(AddressFamily.InterNetwork, 
                                     SocketType.Stream,
                                    ProtocolType.Tcp);
            my_socket.SendTimeout = 5000;


            try  {
                am_valid = false;
                EventWaitHandle handle = new EventWaitHandle(false, EventResetMode.AutoReset);
                AsyncCallback callback = delegate (IAsyncResult res) {
                    am_valid = my_socket.Connected;
                    handle.Set();
                };
                my_socket.BeginConnect(hostName, port, callback, null); 
                handle.WaitOne(5000, false);

                if (!am_valid)
                {
                    ConnectionException exc = 
                        new ConnectionException("Couldn't connect to:" + hostName);
                    if (my_exc_handle != null)
                        my_exc_handle(exc);
                    else
                        throw exc;
                }
            } catch (SocketException e) {
                ConnectionException exc =  new ConnectionException(e.Message);
                if (my_exc_handle != null)
                    my_exc_handle(exc);
                else
                    throw exc;
            }
            my_listen = new Thread(Recv);
            my_listen.IsBackground = true;
            my_listen.Start();

            my_exc_handle = null;
        }

        public delegate void ReceiveCallback(XmlDocument recv, 
                                            Socket socket,
											ref object connection,
											int number);

        /// <summary>
        /// Calls callback everytime a message comes in
        /// This call will be on its own thread.
        /// callback will be passed sock
        /// </summary>
        /// <param name="sock">The socket</param>
        /// <param name="callback">the callback</param>
        public static void Receive(Socket sock, 
                                            ReceiveCallback callback,
											object connection)
        {
            int depth = 0;
            char last = '\0';
			int num = 0;
            StringBuilder incoming = new StringBuilder();
            while (true) {
                byte[] buf = new byte[256];
                int place = 0;
				int received;
				try {
					received = sock.Receive(buf);
				} catch (SocketException) { //happens when you do a Dispose
					return;
				}
                if (received <= 0) {
                    Console.WriteLine("Finished Receiving");
                    return;
                }
                UTF8Encoding encoding = new UTF8Encoding();
                String strbuf = new String(encoding.GetChars(buf));

                int c;
                for (place = c = 0; c < strbuf.Length; ++c) {
                    char ch = strbuf[c];
                    if (ch == '\0')
                    {
                        place = c;
                        break;
                    }
                    if (ch == '<') {
                        //<tag>
                        depth += 1;
                    } else if (ch == '/') {
                        if (last == '<') {
                            //</tag>
                            depth -= 2; ;
                        } 
                    } else if (ch == '?') {
                        if (last == '<') {
                            //<?...?>
                            depth -= 1;
                        }
                    } else if (ch == '>') {
                        if (depth == 0 && last != '?') {
                            //END OF A MESSAGE
                            incoming.Append(strbuf.Substring(place, (c+1)-place));
                            int waste = place;
                            place = c+1;

                            String document = incoming.ToString();

                            XmlDocument doc = new XmlDocument();
                            doc.LoadXml(document);

							int send_num = num;
                            ThreadStart caller = delegate() {
                                callback(doc, sock, ref connection, send_num);
                            };
                            Thread t1 = new Thread(caller);
                            t1.IsBackground = true;
                            t1.Start();

							++num;
                            incoming = new StringBuilder();
                        }
                        if (last == '/') {
                            depth -= 1;
                        }
                    }
                    last = ch;
                }
                if (place == 0)
                {
                    incoming.Append(strbuf);
                }
                else if (c - place > 0)
                {
                    incoming.Append(strbuf.Substring(place, c - place));
                }
            }
        }

       /// <summary>
       /// A private method which runs on a second thread and gets messages.
       /// </summary>
       /// <param name="state">an ignored paramater</param>
        private void Recv(object state)
        {
            Receive(my_socket, HandleRecv, null);
            Console.WriteLine("Done Receiving!!!");
            am_valid = false;
            //^runs forever..
        }
        /// <summary>
        /// Handles messages as they come in.
        /// </summary>
        /// <param name="wire_doc"></param>
        /// <param name="sock"></param>
        private void HandleRecv(XmlDocument wire_doc, Socket sock, 
											ref object connection,
											int num)
        {
            XmlDocument doc = new XmlDocument();
            XmlNode ticket = wire_doc.SelectSingleNode("Message/Ticket");
            XmlNode ourmsg = ticket.PreviousSibling;
            if (ourmsg == null)
                ourmsg = ticket.NextSibling;

            OWEDReply callback;
            lock (my_handles) {
                int number = int.Parse(ticket.Attributes.
                                GetNamedItem("number").Value);
                if (!my_handles.ContainsKey(number)) {
                    ConnectionException exc =  new ConnectionException(
                        "Invalid ticket:" +
                        number.ToString());
                    if (my_exc_handle != null)
                        my_exc_handle(exc);
                    else
                        throw exc;
                }
                callback = my_handles[number];
                my_handles.Remove(number);
            }

            doc.AppendChild(doc.ImportNode(ourmsg, true));
            callback(doc);
            //launch the reply...
        }

		/// <summary>
		/// Synchronous messaging.  Returns the server reply after waiting
		/// indefinitely.
		/// </summary>
		/// <param name="data">The message to send</param>
		/// <returns>The server's reply</returns>
		public XmlDocument Send(XmlDocument data)
		{
            if (!my_socket.Connected)
            {
                ConnectionException exc =
                    new ConnectionException("No longer connected");
                if (my_exc_handle != null)
                    my_exc_handle(exc);
                else
                    throw exc;
            }
            EventWaitHandle my_synchr = new EventWaitHandle(false, EventResetMode.AutoReset);

            bool am_really_done = false;
			XmlDocument ret = null;
			OWEDReply response = delegate(XmlDocument reply) {
				ret = reply;
                am_really_done = true;
				my_synchr.Set();
			};
            if (Send(data, response))
            {
                for (int i = 0; i < 60; ++i)
                { //wait for about 60 seconds, then give up
                    //stop immediately on success
                    if (!am_valid)
                    {
                        ConnectionException exc =
                            new ConnectionException("Server closed connection");
                        if (my_exc_handle != null)
                            my_exc_handle(exc); //not sure this code can work...
                        else
                            throw exc;
                        return null;
                    }
                    my_synchr.WaitOne(1000, false);
                    if (am_really_done)
                    {
                        break; //success
                    }
                    
                }
                //my_synchr.WaitOne(30000, false);
                if (ret == null)
                {
                    ConnectionException exc =
                        new ConnectionException("Request timed out");
                    if (my_exc_handle != null)
                        my_exc_handle(exc);
                    else
                        throw exc;
                }
            }
			return ret;
		}
        /// <summary>
        /// Call this method to send data to the server.
        /// This blocks to send data, but does not wait for a reply.
        /// </summary>
        /// <param name="data">An XmlDocument to send, it may
        /// have only one toplevel element!</param>
        /// <param name="callback">A function to be called when the
        /// send is finished. (on a different thread)</param>
        public bool Send(XmlDocument data, OWEDReply callback)
        {
            lock (this) {
                XmlDocument wire_doc = new XmlDocument();
                wire_doc.AppendChild(wire_doc.CreateElement("Message"));
                XmlNode ticket = wire_doc.CreateElement("Ticket");
                ticket.Attributes.Append(wire_doc.CreateAttribute("number"));
                ticket.Attributes.GetNamedItem("number").
                                        Value = my_number.ToString();
                my_number += 1;
                wire_doc.FirstChild.AppendChild(
                    wire_doc.ImportNode(data.FirstChild, true));
                wire_doc.FirstChild.AppendChild(ticket);

                try {
                    StringBuilder datFactory = new StringBuilder();
                    XmlWriter wr = XmlWriter.Create(datFactory);
                    wire_doc.Save(wr);

                    UTF8Encoding encode = new UTF8Encoding();
                    byte[] wire_dat = encode.GetBytes(datFactory.ToString());
                    lock (my_handles) {
                        my_handles.Add(my_number - 1, callback);
                    }
                    
                    if (my_socket.Send(wire_dat) < wire_dat.Length - 1)
                    { //failed to send..
                        return false;
                    }
                } catch (SocketException e) {
                    lock (my_handles) {
                        my_handles.Remove(my_number - 1);
                    }
                    ConnectionException exc = new ConnectionException(e.Message);
                    if (my_exc_handle != null)
                    {
                        my_exc_handle(exc);
                        return false;
                    }
                    else
                        throw exc;
                    
                }
            }
            return true ;
        }


        
        /*public static void Main(String[] args)
        {
            
        }*/

        #region IDisposable Members

        public void Dispose()
        {
            if (my_socket.Connected)
                my_socket.Disconnect(false);
        }

        #endregion
    }
}

